"""
OPC-UA Connector Implementation

Uses asyncua library for asynchronous OPC-UA client operations.
Supports OPC-UA protocol for industrial automation and process control.
"""

from __future__ import annotations

from datetime import datetime
from typing import Any

from asyncua import Client
from asyncua.ua.uaerrors import UaError, UaStatusCodeError

from app.connectors.base import BaseConnector, ConnectionStatus, DataPoint, DataQuality
from app.connectors.exceptions import (
    ConnectionError as ConnectorConnectionError,
)
from app.connectors.exceptions import (
    ConnectionTimeoutError,
    DeviceOfflineError,
    ProtocolError,
    ReadError,
    WriteError,
)


class OPCUAConnector(BaseConnector):
    """
    OPC-UA protocol connector

    Supports:
    - OPC-UA TCP protocol (opc.tcp://)
    - Anonymous authentication
    - Username/password authentication
    - Certificate-based authentication (future)
    - Read/write operations
    - Browsing address space
    - Subscriptions (future)

    Configuration:
        endpoint_url (str): OPC-UA server endpoint URL (e.g., "opc.tcp://localhost:4840")
        auth_type (str): Authentication type ("anonymous", "username", "certificate")
        username (str, optional): Username for authentication
        password (str, optional): Password for authentication
        security_mode (str, optional): Security mode ("None", "Sign", "SignAndEncrypt")
        security_policy (str, optional): Security policy ("None", "Basic256Sha256", etc.)
        namespace_index (int, optional): Default namespace index (default: 2)
    """

    def __init__(self, config: dict[str, Any]) -> None:
        """
        Initialize OPC-UA connector

        Args:
            config: Connector configuration
                Required:
                    - device_id: Unique device identifier
                    - endpoint_url: OPC-UA server endpoint URL
                Optional:
                    - auth_type: Authentication type (default: "anonymous")
                    - username: Username for authentication
                    - password: Password for authentication
                    - namespace_index: Default namespace index (default: 2)
                    - timeout: Connection timeout in seconds (default: 5)
        """
        # Validate OPC-UA specific configuration BEFORE calling super().__init__()
        if "endpoint_url" not in config:
            raise ValueError("Missing required config key: endpoint_url")

        # Initialize OPC-UA specific attributes BEFORE super().__init__()
        # because _validate_config() will be called by super().__init__()
        self.endpoint_url: str = config["endpoint_url"]
        self.auth_type: str = config.get("auth_type", "anonymous")
        self.username: str | None = config.get("username")
        self.password: str | None = config.get("password")
        self.namespace_index: int = config.get("namespace_index", 2)

        # OPC-UA client instance
        self._client: Client | None = None

        # Call parent constructor (which will call _validate_config())
        super().__init__(config)

        self.logger.info(f"OPC-UA connector initialized for {self.endpoint_url}")

    def _validate_config(self) -> None:
        """
        Validate OPC-UA specific configuration

        Raises:
            ValueError: If configuration is invalid
        """
        super()._validate_config()

        # Validate endpoint URL
        if not self.endpoint_url.startswith("opc.tcp://"):
            raise ValueError(
                f"Invalid endpoint_url: {self.endpoint_url}. Must start with 'opc.tcp://'"
            )

        # Validate authentication configuration
        if self.auth_type == "username":
            if not self.username or not self.password:
                raise ValueError("Username and password are required for 'username' auth_type")

        if self.auth_type not in ["anonymous", "username", "certificate"]:
            raise ValueError(
                f"Invalid auth_type: {self.auth_type}. "
                f"Must be one of: anonymous, username, certificate"
            )

    async def connect(self) -> bool:
        """
        Establish connection to OPC-UA server

        Returns:
            bool: True if connection successful

        Raises:
            ConnectionError: If connection fails
            ConnectionTimeoutError: If connection times out
        """
        try:
            self.status = ConnectionStatus.CONNECTING
            self.logger.info(f"Connecting to OPC-UA server: {self.endpoint_url}")

            # Create client instance
            self._client = Client(url=self.endpoint_url, timeout=self.timeout)

            # Set authentication
            if self.auth_type == "username":
                self._client.set_user(self.username)
                self._client.set_password(self.password)
                self.logger.info(f"Using username/password authentication: {self.username}")

            # Connect to server
            await self._client.connect()

            # Verify connection by reading server status
            server_state = await self._client.get_node("ns=0;i=2259").read_value()  # ServerState
            self.logger.info(f"Connected to OPC-UA server. Server state: {server_state}")

            self.status = ConnectionStatus.CONNECTED
            return True

        except TimeoutError as e:
            self.status = ConnectionStatus.ERROR
            self.logger.error(f"Connection timeout: {e}")
            raise ConnectionTimeoutError(
                f"Connection to {self.endpoint_url} timed out after {self.timeout}s"
            ) from e

        except UaError as e:
            self.status = ConnectionStatus.ERROR
            self.logger.error(f"OPC-UA error during connection: {e}")
            raise ConnectorConnectionError(
                f"Failed to connect to {self.endpoint_url}: {e}"
            ) from e

        except Exception as e:
            self.status = ConnectionStatus.ERROR
            self.logger.error(f"Unexpected error during connection: {e}")
            raise ConnectorConnectionError(
                f"Failed to connect to {self.endpoint_url}: {e}"
            ) from e

    async def disconnect(self) -> bool:
        """
        Disconnect from OPC-UA server

        Returns:
            bool: True if disconnection successful
        """
        try:
            if self._client is not None:
                self.logger.info("Disconnecting from OPC-UA server")
                await self._client.disconnect()
                self._client = None

            self.status = ConnectionStatus.DISCONNECTED
            self.logger.info("Disconnected from OPC-UA server")
            return True

        except Exception as e:
            self.logger.error(f"Error during disconnection: {e}")
            self.status = ConnectionStatus.ERROR
            return False

    async def read_single(self, address: str) -> DataPoint:
        """
        Read a single data point from OPC-UA server

        Args:
            address: Node identifier (e.g., "ns=2;i=1001" or "2:MyVariable")

        Returns:
            DataPoint: Read data point with value, timestamp, and quality

        Raises:
            ReadError: If read operation fails
            DeviceOfflineError: If device is not connected
        """
        if self.status != ConnectionStatus.CONNECTED or self._client is None:
            raise DeviceOfflineError(
                f"Device {self.name} is not connected. Current status: {self.status}"
            )

        try:
            # Get node by address
            node = self._client.get_node(address)

            # Read data value (includes value, timestamp, and quality)
            data_value = await node.read_data_value()

            # Convert OPC-UA quality to our DataQuality enum
            quality = self._convert_quality(data_value.StatusCode)

            # Extract timestamp (prefer source timestamp over server timestamp)
            timestamp = data_value.SourceTimestamp or data_value.ServerTimestamp or datetime.now()

            # Create DataPoint
            data_point = DataPoint(
                value=data_value.Value.Value,  # Actual value
                timestamp=timestamp,
                quality=quality,
                address=address,
                metadata={
                    "status_code": str(data_value.StatusCode),
                    "server_timestamp": data_value.ServerTimestamp.isoformat()
                    if data_value.ServerTimestamp
                    else None,
                },
            )

            self.logger.debug(f"Read from {address}: {data_point.value}")
            return data_point

        except UaStatusCodeError as e:
            self.logger.error(f"OPC-UA status code error reading {address}: {e}")
            raise ReadError(f"Failed to read from {address}: {e}") from e

        except UaError as e:
            self.logger.error(f"OPC-UA error reading {address}: {e}")
            raise ReadError(f"Failed to read from {address}: {e}") from e

        except Exception as e:
            self.logger.error(f"Unexpected error reading {address}: {e}")
            raise ReadError(f"Failed to read from {address}: {e}") from e

    async def read_multiple(self, addresses: list[str]) -> list[DataPoint]:
        """
        Read multiple data points from OPC-UA server

        Args:
            addresses: List of node identifiers

        Returns:
            List[DataPoint]: List of read data points

        Raises:
            ReadError: If read operation fails
        """
        if self.status != ConnectionStatus.CONNECTED or self._client is None:
            raise DeviceOfflineError(
                f"Device {self.name} is not connected. Current status: {self.status}"
            )

        try:
            # Get nodes
            nodes = [self._client.get_node(addr) for addr in addresses]

            # Batch read using OPC-UA ReadValueId service
            data_values = await self._client.read_values(nodes)

            # Convert to DataPoints
            data_points: list[DataPoint] = []
            for i, data_value in enumerate(data_values):
                quality = self._convert_quality(data_value.StatusCode)
                timestamp = (
                    data_value.SourceTimestamp or data_value.ServerTimestamp or datetime.now()
                )

                data_point = DataPoint(
                    value=data_value.Value.Value,
                    timestamp=timestamp,
                    quality=quality,
                    address=addresses[i],
                    metadata={
                        "status_code": str(data_value.StatusCode),
                        "server_timestamp": data_value.ServerTimestamp.isoformat()
                        if data_value.ServerTimestamp
                        else None,
                    },
                )
                data_points.append(data_point)

            self.logger.debug(f"Batch read {len(addresses)} nodes")
            return data_points

        except Exception as e:
            self.logger.error(f"Error in batch read: {e}")
            raise ReadError(f"Failed to read multiple nodes: {e}") from e

    async def write_single(self, address: str, value: Any) -> bool:
        """
        Write a single value to OPC-UA server

        Args:
            address: Node identifier
            value: Value to write

        Returns:
            bool: True if write successful

        Raises:
            WriteError: If write operation fails
            DeviceOfflineError: If device is not connected
        """
        if self.status != ConnectionStatus.CONNECTED or self._client is None:
            raise DeviceOfflineError(
                f"Device {self.name} is not connected. Current status: {self.status}"
            )

        try:
            # Get node
            node = self._client.get_node(address)

            # Write value
            await node.write_value(value)

            self.logger.debug(f"Wrote to {address}: {value}")
            return True

        except UaStatusCodeError as e:
            self.logger.error(f"OPC-UA status code error writing to {address}: {e}")
            raise WriteError(f"Failed to write to {address}: {e}") from e

        except UaError as e:
            self.logger.error(f"OPC-UA error writing to {address}: {e}")
            raise WriteError(f"Failed to write to {address}: {e}") from e

        except Exception as e:
            self.logger.error(f"Unexpected error writing to {address}: {e}")
            raise WriteError(f"Failed to write to {address}: {e}") from e

    async def write_multiple(self, data: dict[str, Any]) -> bool:
        """
        Write multiple values to OPC-UA server

        Args:
            data: Dictionary of {address: value} pairs

        Returns:
            bool: True if all writes successful

        Raises:
            WriteError: If any write operation fails
        """
        if self.status != ConnectionStatus.CONNECTED or self._client is None:
            raise DeviceOfflineError(
                f"Device {self.name} is not connected. Current status: {self.status}"
            )

        try:
            # Get nodes and values
            nodes = [self._client.get_node(addr) for addr in data]
            values = list(data.values())

            # Batch write using OPC-UA WriteValue service
            # Note: asyncua doesn't have a direct batch write method,
            # so we'll use sequential writes wrapped in asyncio.gather for parallelism
            write_tasks = [node.write_value(value) for node, value in zip(nodes, values)]
            await asyncio.gather(*write_tasks)

            self.logger.debug(f"Batch wrote {len(data)} nodes")
            return True

        except Exception as e:
            self.logger.error(f"Error in batch write: {e}")
            raise WriteError(f"Failed to write multiple nodes: {e}") from e

    async def browse_nodes(self, parent_node_id: str = "i=85") -> list[dict[str, Any]]:
        """
        Browse child nodes of a parent node

        Args:
            parent_node_id: Parent node identifier (default: Objects folder i=85)

        Returns:
            List of node information dictionaries

        Raises:
            ProtocolError: If browsing fails
        """
        if self.status != ConnectionStatus.CONNECTED or self._client is None:
            raise DeviceOfflineError(
                f"Device {self.name} is not connected. Current status: {self.status}"
            )

        try:
            parent_node = self._client.get_node(parent_node_id)
            children = await parent_node.get_children()

            nodes_info: list[dict[str, Any]] = []
            for child in children:
                node_id = child.nodeid.to_string()
                browse_name = await child.read_browse_name()
                display_name = await child.read_display_name()

                nodes_info.append(
                    {
                        "node_id": node_id,
                        "browse_name": browse_name.Name,
                        "display_name": display_name.Text,
                    }
                )

            self.logger.debug(f"Browsed {len(nodes_info)} child nodes of {parent_node_id}")
            return nodes_info

        except Exception as e:
            self.logger.error(f"Error browsing nodes: {e}")
            raise ProtocolError(f"Failed to browse nodes: {e}") from e

    async def health_check(self) -> bool:
        """
        Perform health check by reading server status

        Returns:
            bool: True if server is reachable and healthy
        """
        if self.status != ConnectionStatus.CONNECTED or self._client is None:
            return False

        try:
            # Read server status node (ns=0;i=2259)
            server_state_node = self._client.get_node("ns=0;i=2259")
            server_state = await server_state_node.read_value()

            # ServerState: 0=Running, 1=Failed, 2=NoConfiguration, etc.
            is_healthy = server_state == 0

            if not is_healthy:
                self.logger.warning(f"Server health check failed. State: {server_state}")

            return is_healthy

        except Exception as e:
            self.logger.error(f"Health check failed: {e}")
            return False

    def _convert_quality(self, status_code: Any) -> DataQuality:
        """
        Convert OPC-UA StatusCode to DataQuality enum

        Args:
            status_code: OPC-UA StatusCode object

        Returns:
            DataQuality: Converted quality value
        """
        # OPC-UA StatusCode quality bits
        # Good: 0x00000000
        # Uncertain: 0x40000000
        # Bad: 0x80000000

        if status_code.is_good():
            return DataQuality.GOOD
        elif status_code.is_uncertain():
            return DataQuality.UNCERTAIN
        else:
            return DataQuality.BAD


# Import asyncio here to avoid circular import
import asyncio
